﻿namespace NServiceBus.Core.Tests.Routing;

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Extensibility;
using NServiceBus.Logging;
using NServiceBus.Routing;
using NUnit.Framework;
using Testing;
using Transport;
using Unicast.Messages;
using Unicast.Subscriptions;
using Unicast.Subscriptions.MessageDrivenSubscriptions;

[TestFixture]
public class UnicastPublisherRouterTests
{
    UnicastPublishRouter router;
    MessageMetadataRegistry metadataRegistry;
    EndpointInstances endpointInstances;
    FakeSubscriptionStorage subscriptionStorage;
    static StringBuilder logStatements = new StringBuilder();

    [Test]
    public async Task When_subscriber_does_not_define_logical_endpoint_should_send_event_to_each_address()
    {
        subscriptionStorage.Subscribers.Add(new Subscriber("address1", null));
        subscriptionStorage.Subscribers.Add(new Subscriber("address2", null));

        var routes = await router.Route(typeof(Event), new DistributionPolicy(), new TestableOutgoingPublishContext());

        var destinations = routes.Select(ExtractDestination).ToList();
        Assert.That(destinations.Count, Is.EqualTo(2));
        Assert.That(destinations, Does.Contain("address1"));
        Assert.That(destinations, Does.Contain("address2"));
    }

    [Test]
    public async Task When_multiple_subscribers_for_logical_endpoints_should_route_event_to_a_single_instance_of_each_logical_endpoint()
    {
        var sales = "Sales";
        var shipping = "Shipping";
        subscriptionStorage.Subscribers.Add(new Subscriber("sales1", sales));
        subscriptionStorage.Subscribers.Add(new Subscriber("sales2", sales));
        subscriptionStorage.Subscribers.Add(new Subscriber("shipping1", shipping));
        subscriptionStorage.Subscribers.Add(new Subscriber("shipping2", shipping));

        var routes = (await router.Route(typeof(Event), new DistributionPolicy(), new TestableOutgoingPublishContext())).ToArray();

        var destinations = routes.Select(ExtractDestination).ToList();
        Assert.That(destinations.Count, Is.EqualTo(2));
        Assert.That(destinations, Does.Contain("sales1"));
        Assert.That(destinations, Does.Contain("shipping1"));
    }

    [Test]
    public async Task Should_not_route_multiple_copies_of_message_to_one_physical_destination()
    {
        subscriptionStorage.Subscribers.Add(new Subscriber("address", null));
        subscriptionStorage.Subscribers.Add(new Subscriber("address", null));
        subscriptionStorage.Subscribers.Add(new Subscriber("address", "sales"));
        subscriptionStorage.Subscribers.Add(new Subscriber("address", "sales"));
        subscriptionStorage.Subscribers.Add(new Subscriber("address", "shipping"));

        var routes = await router.Route(typeof(Event), new DistributionPolicy(), new TestableOutgoingPublishContext());

        using (Assert.EnterMultipleScope())
        {
            Assert.That(routes.Count(), Is.EqualTo(1));
            Assert.That(ExtractDestination(routes.Single()), Is.EqualTo("address"));
        }
    }

    [Test]
    public async Task Should_not_route_events_to_configured_endpoint_instances()
    {
        var logicalEndpoint = "sales";
        subscriptionStorage.Subscribers.Add(new Subscriber("address", logicalEndpoint));
        endpointInstances.AddOrReplaceInstances("A",
        [
            new EndpointInstance(logicalEndpoint, "1"),
            new EndpointInstance(logicalEndpoint, "2")
        ]);

        var routes = await router.Route(typeof(Event), new DistributionPolicy(), new TestableOutgoingPublishContext());

        using (Assert.EnterMultipleScope())
        {
            Assert.That(routes.Count(), Is.EqualTo(1));
            Assert.That(ExtractDestination(routes.First()), Is.EqualTo("address"));
        }
    }

    [Test]
    public async Task Should_return_empty_list_when_no_routes_found()
    {
        var routes = await router.Route(typeof(Event), new DistributionPolicy(), new TestableOutgoingPublishContext());

        Assert.That(routes, Is.Empty);
    }

    [Test]
    public async Task When_no_subscribers_found_Should_log_at_debug_level()
    {
        await router.Route(typeof(Event), new DistributionPolicy(), new TestableOutgoingPublishContext());

        var warning = $"No subscribers found for the event of type {typeof(Event).FullName}.";

        Assert.That(logStatements.ToString(), Does.Contain(warning));
        Assert.That(logStatements.ToString(), Does.Contain(" DEBUG "));
    }

    static string ExtractDestination(UnicastRoutingStrategy route)
    {
        var headers = new Dictionary<string, string>();
        var addressTag = (UnicastAddressTag)route.Apply(headers);
        return addressTag.Destination;
    }

    [OneTimeSetUp]
    public void LoggerSetup()
    {
        LogManager.Use<TestingLoggerFactory>()
            .WriteTo(new StringWriter(logStatements));
    }

    [SetUp]
    public void Setup()
    {
        metadataRegistry = new MessageMetadataRegistry(_ => true, true);
        endpointInstances = new EndpointInstances();
        subscriptionStorage = new FakeSubscriptionStorage();
        router = new UnicastPublishRouter(
            metadataRegistry,
            new FakeAddressResolver(),
            subscriptionStorage);

        logStatements.Clear();
    }

    class FakeSubscriptionStorage : ISubscriptionStorage
    {
        public List<Subscriber> Subscribers { get; } = [];
        public Task Subscribe(Subscriber subscriber, MessageType messageType, ContextBag context, CancellationToken cancellationToken = default)
        {
            throw new NotImplementedException();
        }

        public Task Unsubscribe(Subscriber subscriber, MessageType messageType, ContextBag context, CancellationToken cancellationToken = default)
        {
            throw new NotImplementedException();
        }

        public Task<IEnumerable<Subscriber>> GetSubscriberAddressesForMessage(IEnumerable<MessageType> messageTypes, ContextBag context, CancellationToken cancellationToken = default)
        {
            return Task.FromResult<IEnumerable<Subscriber>>(Subscribers);
        }
    }

    class FakeAddressResolver : ITransportAddressResolver
    {
        public string ToTransportAddress(QueueAddress queueAddress) => string.Empty;
    }

    class Event : IEvent
    {
    }
}